package com.blueskything.practice.lesson01;

import java.util.Random;

/**
 * 感知机
 * 线性模型：y=wx+b
 * 理论详见：https://www.bilibili.com/cheese/play/ss17600
 *
 * @since 2024/8/26.
 */
public class PreBodyV2 {
    // 权重参数
    private double w;
    // 偏值
    private double b;
    private double x;
    private double y;
    // 超参数
    // 学习率 (0<studyPad<1)
    // 加快模型的收敛速度
    private double studyPad = 0.55;

    private int normalizeX = 1;
    private int normalizeY = 1;

    public PreBodyV2() {
        Random random = new Random();
        w = random.nextDouble();
        b = random.nextDouble();
    }

    public PreBodyV2(double studyPad, int normalizeX, int normalizeY) {
        this();
        this.studyPad = studyPad;
        this.normalizeX = normalizeX;
        this.normalizeY = normalizeY;
    }

    /**
     * 投喂参数进行训练
     *
     * @param parameter 运算参数(x)
     *
     * @return AI估算值y
     */
    public double insertParameter(double parameter) {
        // 对parameter进行归一化处理，防止y的数值出现溢出
        this.x = normalizeX(parameter);
        // 计算出y的值
        this.y = w * x + b;
        // 输出结果反归一化
        return unNormalizeY(y);
    }

    /**
     * 对x进行归一化处理
     *
     * @param x
     *
     * @return
     */
    public double normalizeX(double x) {
        return x / normalizeX;
    }

    /**
     * 对y进行归一化处理
     *
     * @param y
     *
     * @return
     */
    public double normalizeY(double y) {
        return y / normalizeY;
    }

    /**
     * 对y进行 反归一化处理
     *
     * @param y
     *
     * @return
     */
    public double unNormalizeY(double y) {
        return y * normalizeY;
    }

    /**
     * 通过梯度下降来 调整w,b
     *
     * @param expect 样本中的真实值
     */
    public void backError(double expect) {
        // 对期望结果进行归一化处理
        expect = normalizeY(expect);
        // AI推断结果与期望结果的误差
        // 误差值存在正负，因此它决定了梯度的方向
        // studyPad用于调整变化的幅度
        double error = (expect - y) * studyPad;
        // 求w对误差的影响，所以对w求偏导-> w'=1*w^(1-1)*x => x
        // w的偏导数为 x
        // w的梯度变化 = 误差 * w的偏导数(x)
        double subW = error * x;
        // 求b对误差的影响，所以对b求偏导,线性函数中常量项的偏导数就是它的符号，±1
        // b的偏导数为 +1
        // b的梯度变化 = 误差 * b的偏导数(+1)
        double subB = error * (1);
        // 更新w、b
        this.w += subW;
        this.b += subB;
    }

    /**
     * 给定x推测出y的值
     *
     * @param x
     *
     * @return y
     */
    public double computeResult(double x) {
        return unNormalizeY(w * (normalizeX(x)) + b);
    }

    @Override
    public String toString() {
        return String.format("y=%s*X %s %s", w, b > 0 ? '+' : '-', Math.abs(b));
    }

    /**
     * 投喂参数及调整误差
     *
     * @param s     面积
     * @param price 价格
     *
     * @return AI预测的价格
     */
    public double insertParameter(double s, double price) {
        // 投喂参数
        double aiResult = insertParameter(s);
        // 调整误差
        backError(price);

        // 返回ai预测值
        return aiResult;
    }
}
